using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;

using Util;

namespace DebugCompanion.Engine
{
	static class AssemblyProcessor
	{
		public static string GenerateDebugAssembly(AssemblyInfo asmInfo, string targetBasePath)
		{
			lock (_sync)
			{
				_terminate = false;
			}
			string targetPath = targetBasePath + @"\" + asmInfo.DisplayName;
			try
			{
				if (Directory.Exists(targetPath))
				{
					foreach (string s in Directory.GetFiles(targetPath))
					{
						File.Delete(s);
					}
					foreach (string s in Directory.GetDirectories(targetPath))
					{
						Directory.Delete(s, true);
					}
				}
				Directory.CreateDirectory(targetPath);

				string disassemblyFileName = Disassemble(asmInfo.CodeBase, targetPath);
				FileStreamEx stream = new FileStreamEx(disassemblyFileName);
				PatchVersion(stream, asmInfo.Version);
				stream.Save(disassemblyFileName);

				List<string> names = new List<string>();
				names.Add(disassemblyFileName);
				Assemble(names, targetPath);

				Cleanup(targetPath);
				string newBinFileName = targetPath + "\\" + Path.GetFileName(asmInfo.CodeBase);
				return newBinFileName;
			}
			catch (Exception exc)
			{
				try
				{
					Directory.Delete(targetPath, true);
				}
				catch (Exception)
				{
				}
				throw;
			}
		}

		private static object _sync = new object();
		private static volatile bool _terminate = false;
		private static volatile Process _curProcess = null;

		public static void TerminateProcessing()
		{
			lock (_sync)
			{
				_terminate = true;
				if (_curProcess != null)
				{
					_curProcess.Close();
					_curProcess = null;
				}
			}
		}

		public static void DisableStrongNameValidation(string asmPath)
		{
			string exePath = EnvironmentUtil.GetDotNetSdk2Path() + @"\Bin\sn.exe";
			string args = string.Format("-Vr \"{0}\"", asmPath);
			ExecuteCommand(exePath, args);
		}

		static string GetName(string baseName, int i)
		{
			string res = Path.GetDirectoryName(baseName) + "\\" + Path.GetFileNameWithoutExtension(baseName) +
				i.ToString() + Path.GetExtension(baseName);
			return res;
		}

		public static void PatchVersion(StreamEx stream, Version version)
		{
			while (true)
			{
				ParsingUtil.SkipToIncluding(stream, ".assembly ");
				if (!ParsingUtil.TryRead(stream, "extern"))
					break;
			}

			ParsingUtil.SkipToIncluding(stream, ".ver ");

			int versionStartPos = stream.Position;
			string oldVersion = ParsingUtil.Capture(stream, delegate(StreamEx s) { ParsingUtil.SkipTo(s, "\r\n"); });
			if (oldVersion.Replace(':', '.') != version.ToString())
				throw new Exception("Disassembled file version doesn't match");
			string newVersion = new Version(version.Major, version.Minor, version.Build, version.Revision + 1).ToString();
			newVersion = newVersion.Replace('.', ':');

			stream.Seek(versionStartPos);
			stream.Delete(oldVersion.Length);
			stream.Insert(newVersion);
			GC.Collect();
		}

		static void Cleanup(string path)
		{
			foreach (string s in Directory.GetFiles(path))
			{
				string extension = Path.GetExtension(s);
				if (extension != ".pdb" && extension != ".dll" && extension != ".il")
					File.Delete(s);
			}
			foreach (string s in Directory.GetDirectories(path))
			{
				Cleanup(s);
			}
		}

		static string GenerateKeyFile(string targetPath)
		{
			string fileName = targetPath + "\\key.snk";
			ExecuteCommand(EnvironmentUtil.GetDotNetSdk2Path() + @"\bin\sn.exe", "-k " + fileName);
			return fileName;
		}


		static string Disassemble(string binFileName, string targetPath)
		{
			string resFileName = Path.GetFileNameWithoutExtension(binFileName) + ".il";
			return Disassemble(binFileName, targetPath, resFileName);
		}

		static string Disassemble(string binFileName, string targetPath, string fileName)
		{
			string exePath = EnvironmentUtil.GetDotNetSdk2Path() + @"\Bin\ildasm.exe";
			string resFileName = targetPath + @"\" + fileName;
			string args = string.Format("\"{0}\" /TEXT /CAVERBAL /TYPELIST /OUT=\"{1}\"", binFileName, resFileName);
			ExecuteCommand(exePath, args);
			return resFileName;
		}

		static void Assemble(IList<string> srcFileNames, string resName)
		{
			Assemble(srcFileNames, resName, null);
		}

		static void Assemble(IList<string> srcFileNames, string resName, string keyFile)
		{
			string exePath = EnvironmentUtil.GetDotNet2Path() + @"\ilasm.exe";
			string srcFiles = StringUtil.Join<string>(" ", srcFileNames,
				delegate(string val) { return "\"" + val + "\""; });
			string args = string.Format("{0} /DLL /DEBUG", srcFiles);
			if (!string.IsNullOrEmpty(keyFile))
				args += string.Format(" /KEY=\"{0}\"", keyFile);
			ExecuteCommand(exePath, args);
		}

		static string ExecuteCommand(string startupFile, string args)
		{
			ProcessStartInfo info = new ProcessStartInfo(startupFile, args);
			info.CreateNoWindow = true;
			info.UseShellExecute = false;
			info.RedirectStandardInput = true;
			info.RedirectStandardOutput = true;
			info.RedirectStandardError = true;
			Process proc = Process.Start(info);

			StringBuilder txtBuf = new StringBuilder();
			StringBuilder errBuf = new StringBuilder();

			MethodInvoker txtReader = delegate()
			{
				string line;
				while (!proc.HasExited && (line = proc.StandardOutput.ReadLine()) != null)
					txtBuf.Append(line);
				Console.WriteLine();
			};
			txtReader.BeginInvoke(null, null);

			MethodInvoker errReader = delegate()
			{
				string line;
				while (!proc.HasExited && (line = proc.StandardError.ReadLine()) != null)
					errBuf.Append(line);
				Console.WriteLine();
			};
			errReader.BeginInvoke(null, null);

			lock (_sync)
			{
				if (_terminate)
					throw new AbortException();
				_curProcess = proc;
			}
			proc.WaitForExit();
			lock (_sync)
			{
				if (_terminate)
					throw new AbortException();
			}
			string text = txtBuf.ToString();
			string err = errBuf.ToString();
			if (proc.ExitCode != 0 || !string.IsNullOrEmpty(err))
			{
				if (!string.IsNullOrEmpty(err))
					throw new CommandLineException(err);
				else
					throw new CommandLineException(text);
			}

			return text;
		}

		public class AbortException : Exception
		{
			
		}
	}
}
